﻿using System.IO;
using System.Xml;
using XNodeType = System.Xml.XmlNodeType;
using SCG = System.Collections.Generic;

using Nemerle.Utility;
using Nemerle.Collections;
using Nemerle.Imperative;


namespace Nemerle.Xml
{
  using DocumentElement;
  using Node;
  using Namespace;

  /// <summary>
  /// Immutable class to store ns aliases and read/write xml
  /// </summary>
  public sealed class Context
  {
    [Accessor] static _default    : Context                         = Context();
                      _aliases    : SCG.IDictionary[string, string] = SCG.Dictionary();
                      _namespaces : SCG.IDictionary[string, string] = SCG.Dictionary();

    public this () { }

    public this(aliases : list[string * string])
    {
      aliases.Iter(Add(_, _, true));
    }

    public this(aliases : SCG.IDictionary[string, string])
    {
      aliases.Iter(x => Add(x.Key, x.Value, false));
    }

    Add(alias : string, ns : string, check = true) : void
    {
      when (check && _aliases.ContainsKey(alias))
        throw System.ArgumentException("alias", $"Duplicate alias {$alias} detected!");

      _aliases.Add(alias, ns);

      when (!_namespaces.ContainsKey(ns))
        _namespaces.Add(ns, alias);
    }

    public AddNsAlias(alias : string, ns : string) : Context
    {
      when (_aliases.ContainsKey(alias))
        throw System.ArgumentException("alias", $"Duplicate alias {$alias} detected!");

      def newContext = Context(_aliases);
      newContext.Add(alias, ns, false);
      newContext
    }

    public LookupAlias(ns : string) : option[string]
    {
      mutable alias = null;
      if (_namespaces.TryGetValue(ns, out alias)) Some(alias) else None()
    }

    public LookupNs(alias : string) : string
    {
      mutable ns = null;
      if (_aliases.TryGetValue(alias, out ns))
        ns
      else
        string.Empty
    }

    public FromString(input : string, ignoreWhitespace = false) : ElementBase
    {
      using (stringReader = StringReader(input), reader = XmlTextReader(stringReader))
          Read(reader, ignoreWhitespace)
    }

    public Read(reader : XmlReader, ignoreWhitespace = false) : ElementBase
    {
      def getName(name)
      {
        def i = name.IndexOf(':');
        if (i >= 0) name.Substring(i + 1) else name
      }

      def getNs(uri : string) : Namespace
      {
        if (!string.IsNullOrEmpty(uri))
        {
          def alias = this.LookupAlias(uri);
          if (alias.IsNone) Uri(uri) else UriAlias(alias.Value)
        }
        else Empty()
      }

      def readAttrs(attrs = SCG.List())
      {
        if (reader.MoveToNextAttribute())
        {
          def (name, prefix, uri) = (reader.Name, reader.Prefix, reader.NamespaceURI);

          when (!name.StartsWith("xmlns"))
            attrs.Add(Attr(getNs(uri), prefix, getName(name), reader.Value));

          readAttrs(attrs)
        }
        else
          attrs.ToList()
      }

      def readProcInstrs(instructions = [])
      {
        match(reader.NodeType)
        {
          | ProcessingInstruction
          | XmlDeclaration =>
              def res = ProcInstruction(reader.Name, reader.Value) :: instructions;
              if (reader.Read())
                readProcInstrs(res)
              else
                res

          | Whitespace
          | SignificantWhitespace => _ = reader.Read(); readProcInstrs(instructions)
          | _ => instructions
        }
      }

      def readNodes(nodes = [])
      {
        def reader = reader;
        def nodeType = reader.NodeType;
        def res = match (nodeType)
        {
          | XNodeType.Element =>
            def (name, prefix, uri) = (reader.Name, reader.Prefix, reader.NamespaceURI);
            def empty = reader.IsEmptyElement;

            Element(getNs(uri), prefix, getName(name), readAttrs(),
            if (empty) []
            else { _ = reader.Read(); readNodes()}) :: nodes

          | XNodeType.Text => Text(reader.Value) :: nodes
          | XNodeType.CDATA => Text(reader.Value) :: nodes
          | XNodeType.SignificantWhitespace
          | XNodeType.Whitespace =>
            if (ignoreWhitespace) nodes else Whitespace(reader.Value) :: nodes

          | _ => nodes
        }

        if (!reader.Read() || nodeType == XNodeType.EndElement)
          res.Reverse()
        else
          readNodes(res)
      }

      if (reader.Read())
        Document(readProcInstrs().Reverse(), readNodes().Reverse())
      else
        Document([], [])
    }

    public ToString(element : ElementBase, writeWhitespaces = false) : string
    {
      using (stringWriter = StringWriter())
        using (writer = XmlTextWriter(stringWriter))
        {
          writer.Formatting = Formatting.Indented;
          Write(element, writer, writeWhitespaces);
          stringWriter.ToString()
        }
    }

    public Write(element : ElementBase, writer : XmlWriter, writeWhitespaces = false) : void
    {
      def getNs(ns)
      {
        | Uri(uri) => uri
        | UriAlias(alias) => this.LookupNs(alias)
        | Empty => ""
      }

      def writeAttrs(attrs)
      {
        attrs.Iter( x => writer.WriteAttributeString(x.Prefix, x.Name, getNs(x.Ns), x.Value) )
      }

      def writeProcInstrs(instructions)
      {
        foreach (ProcInstruction(name, value) in instructions)
          writer.WriteProcessingInstruction(name, value);
      }

      def writeNodes(elements)
      {
        foreach (elem in elements)
        {
          | Text(text)                  => writer.WriteString(text)
          | Whitespace(_text)           => when (writeWhitespaces) writer.WriteWhitespace(_text)

          | Element(ns, prefix, name, attrs, [])    => writer.WriteStartElement(prefix, name, getNs(ns));
                                                       writeAttrs(attrs);
                                                       writer.WriteEndElement()

          | Element(ns, prefix, name, attrs, nodes) => writer.WriteStartElement(prefix, name, getNs(ns));
                                                       writeAttrs(attrs);
                                                       writeNodes(nodes);
                                                       writer.WriteFullEndElement()
        }
      }

      match (element)
      {
        | Document(pi, nodes) => writeProcInstrs(pi); writeNodes(nodes)
        | ProcInstruction as pi => writeProcInstrs([pi])
        | _ => writeNodes([element :> Node])
      }
    }
  }
}
